﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Net.Mime;
using System.Net.Security;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text.RegularExpressions;
using System.Threading;
using SharpBits.Base;
using WinServiceBase;
using WinServiceBase.Utils;

namespace WinServiceBase.WinServices
{
    public class DownloadsThread : TimerThread
    {
        public const string DownloadRegexPattern = @"\s*(?<comment>#)?\s*(?<job>(?:""(?<url>[^\[\]]*?)""|(?<url>[^\s\[\]]+))\s*(?:""(?<saveAs>[^\[\]]*?)""|(?<saveAs>[^\s\[\]]+)?))\s*(?<status>\[.*\])?";
        public static Regex DownloadRegexObj = new Regex(DownloadRegexPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline);

        private Regex filesRegexObj;
        private BitsManager bitsMng;
        private Dictionary<JobFile, Guid> ownFiles;

        public DownloadsThread(string downloadsFilesRegex, string logsfile, string inputDir, string outputDir)
        {
            this.filesRegexObj = new Regex(downloadsFilesRegex, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline);
            this.LogsFile = logsfile;
            this.DownloadsInputDir = inputDir;
            this.DownloadsOutputDir = outputDir;

            this.Init();
        }
        public string LogsFile
        {
            get;
            private set;
        }
        public string DownloadsFilesRegex
        {
            get { return this.filesRegexObj.ToString(); }
        }
        public string DownloadsInputDir
        {
            get;
            private set;
        }
        public string DownloadsOutputDir
        {
            get;
            private set;
        }
        public void AddLog(params string[] logTexts)
        {
            LogService.LogToFile(this.LogsFile, logTexts);
        }
        public override void Start()
        {
            base.Start();

            this.GetOwnJobs().Foreach(curJob =>
            {
                if(curJob.Value.State == JobState.Suspended)
                    curJob.Value.Resume();
            });
        }
        public override void Stop()
        {
            base.Stop();

            this.GetOwnJobs().Foreach(curJob =>
            {
                if(curJob.Value.State == JobState.Transferring)
                    curJob.Value.Suspend();
            });
        }

        protected override void Run(object source)
        {
            Dictionary<JobFile, Guid> jobsCopy = new Dictionary<JobFile, Guid>(this.ownFiles);

            foreach(var curFile in Directory.GetFiles(this.DownloadsInputDir))
            {
                if(!this.filesRegexObj.IsMatch(curFile))
                    continue;

                StreamWriter writer = new StreamWriter(new MemoryStream()) { AutoFlush = true };
                try
                {
                    IEnumerable<string> lines = null;
                    
                    try { lines = File.ReadLines(curFile); }
                    catch(Exception ex)
                    {
                        this.AddLog(string.Format("Error reading from input file '{0}': {1}", curFile, ex.Message));
                        continue;
                    }

                    bool modified = false;
                    foreach(var curLine in lines)
                    {
                        var curMatch = DownloadRegexObj.Match(curLine);
                        if(curMatch.Groups["comment"] != null && !string.IsNullOrEmpty(curMatch.Groups["comment"].Value))
                        {
                            writer.WriteLine(curLine);
                            continue;
                        }

                        modified = true;
                        var jobMatch = curMatch.Groups["job"];
                        var urlMatch = curMatch.Groups["url"];
                        if(jobMatch == null 
                            || string.IsNullOrWhiteSpace(jobMatch.Value)
                            || urlMatch == null 
                            || string.IsNullOrWhiteSpace(urlMatch.Value))
                        {
                            writer.WriteLine(jobMatch.Value.Trim() + " [Status: Syntax error]");
                            continue;
                        }

                        Uri uri = null;
                        try
                        {
                            uri = new Uri(urlMatch.Value.Trim());
                        }
                        catch
                        {
                            writer.WriteLine(jobMatch.Value.Trim() + " [Status: Invalid url]");
                            continue;
                        }

                        var saveAsMatch = curMatch.Groups["saveAs"];
                        string saveAs = "";
                        if(saveAsMatch == null || string.IsNullOrWhiteSpace(saveAsMatch.Value))
                            saveAs = Path.GetFileName(uri.LocalPath);
                        else
                        {
                            saveAs = saveAsMatch.Value.Trim();
                            if(!Path.IsPathRooted(saveAs))
                                saveAs = Path.Combine(this.DownloadsOutputDir, saveAs);

                            if(Directory.Exists(saveAs))
                                saveAs = Path.Combine(saveAs, Path.GetFileName(uri.LocalPath));
                        }

                        saveAs = saveAs.ToLowerInvariant();
                        
                        JobFile jFile = new JobFile() {
                            RemotePath = uri.AbsoluteUri.ToLowerInvariant(),
                            LocalPath = saveAs.ToLowerInvariant()
                        };

                        BitsJob job = this.ownFiles.ContainsKey(jFile) ? this.GetJobByKey(this.ownFiles[jFile]) : null;
                        if(job == null)
                        {
                            job = this.CreateJob(jFile);
                            writer.WriteLine(jobMatch.Value.Trim() + " [Status: Added]");
                        }
                        else
                            jobsCopy.Remove(jFile);

                        switch(job.State)
                        {
                            case JobState.Suspended:
                                job.Resume();
                                break;
                            case JobState.Transferred:
                                this.CompleteJob(jFile, job);
                                writer.WriteLine("#" + jobMatch.Value.Trim() + " [Status: Completed]");
                                break;
                            case JobState.Error:
                                this.CancelJob(jFile, job);
                                writer.WriteLine("#" + jobMatch.Value.Trim() + " [Status: Error]");
                                break;
                            default:
                                var bFile = job.EnumFiles()[0];
                                long precent = bFile.Progress.BytesTotal > 0
                                               ? (long)((decimal)bFile.Progress.BytesTransferred / (decimal)bFile.Progress.BytesTotal * (decimal)100)
                                               : 0;
                                writer.WriteLine(string.Format("{0} [Status: {1} {2}% ({3} of {4})]", jobMatch.Value.Trim(), job.State, precent, bFile.Progress.BytesTransferred.AsStorageUnit(), bFile.Progress.BytesTotal.AsStorageUnit()));
                                break;
                        }
                    }

                    if(modified && writer.BaseStream.Length > 0)
                    {
                        try{ File.WriteAllBytes(curFile, (writer.BaseStream as MemoryStream).ToArray()); }
                        catch(Exception ex)
                        {
                            this.AddLog(string.Format("Error writing jobs status to input file '{0}': {1}", curFile, ex.Message));
                        }
                    }
                }
                finally
                {
                    writer.Close();
                }
            }

            jobsCopy.Foreach(jFile => {
                BitsJob job = this.GetJobByKey(jFile.Value);
                if(job != null)
                    this.CancelJob(jFile.Key, job);
            });
        }
                
        private BitsJobs GetOwnJobs()
        {
            return this.bitsMng.EnumJobs(JobOwner.CurrentUser);
        }
        private BitsJob GetJobByDisplayName(string jobName)
        {
            return this.GetOwnJobs().Where(curJob => curJob.Value.DisplayName == jobName).Select(curJob => curJob.Value).FirstOrDefault();
        }
        private BitsJob GetJobByKey(Guid jobKey)
        {
            var ownJobs = this.GetOwnJobs();
            return ownJobs.ContainsKey(jobKey) ? ownJobs[jobKey] : null;
        }
        private BitsJob CreateDownloadsJob(string jobName)
        {
            var result = this.GetJobByDisplayName(jobName);
            if(result == null)
                result = this.bitsMng.CreateJob(jobName, JobType.Download);

            return result;
        }
        private BitsJob CreateJob(JobFile jFile)
        {
            var job = this.bitsMng.CreateJob(jFile.ToString(), JobType.Download);
            job.AddFile(jFile.RemotePath, jFile.LocalPath);
            this.ownFiles.Add(jFile, job.JobId);
            this.AddLog("Job created: " + jFile.ToString());

            return job;
        }
        private void CancelJob(JobFile jFile, BitsJob job)
        {
            job.Cancel();
            this.GetOwnJobs().Remove(job.JobId);
            if(this.ownFiles.ContainsKey(jFile))
                this.ownFiles.Remove(jFile);

            this.AddLog("Job canceled: " + jFile.ToString());
        }
        private void CompleteJob(JobFile jFile, BitsJob job)
        {
            job.Complete();
            this.GetOwnJobs().Remove(job.JobId);
            if(this.ownFiles.ContainsKey(jFile))
                this.ownFiles.Remove(jFile);

            this.AddLog("Job completed: " + jFile.ToString());
        }
        private Dictionary<JobFile, Guid> LoadJobQueue()
        {
            Dictionary<JobFile, Guid> result = new Dictionary<JobFile, Guid>();
            foreach(var curJob in this.GetOwnJobs())
                foreach(var curFile in curJob.Value.EnumFiles())
                    result.Add(new JobFile()
                    {
                        RemotePath = curFile.RemoteName,
                        LocalPath = curFile.LocalName
                    }, curJob.Key);

            return result;
        }
        private void Init()
        {
            try
            {

                if(!Directory.Exists(this.DownloadsInputDir))
                    Directory.CreateDirectory(this.DownloadsInputDir);
            }
            catch(Exception ex)
            {
                this.AddLog("Error creating the downloads input directory: " + ex.Message);
                throw ex;
            }

            try
            {

                if(!Directory.Exists(this.DownloadsOutputDir))
                    Directory.CreateDirectory(this.DownloadsOutputDir);
            }
            catch(Exception ex)
            {
                this.AddLog("Error creating the downloads output directory: " + ex.Message);
                throw ex;
            }

            try { this.bitsMng = new BitsManager(); }
            catch(Exception ex)
            {
                this.AddLog("Error conecting to the BITS services: " + ex.Message);
                throw ex;
            }

            this.ownFiles = this.LoadJobQueue();
        }
    }

    public class JobFile
    {
        public string RemotePath
        { get; set; }
        public string LocalPath
        { get; set; }
        public Guid AssocJobKey
        { get; set; }
        public override int GetHashCode()
        {
            return (this.RemotePath + "=>" + this.LocalPath).GetHashCode();
        }
        public override bool Equals(object obj)
        {
            JobFile df = obj as JobFile;
            return obj != null && this.GetHashCode() == obj.GetHashCode();
        }
        public override string ToString()
        {
            return string.Format("{0} {1} as {2}", JobType.Download, this.RemotePath, this.LocalPath);
        }
    }
}